package model;
/**
 * The hand is a list of the cards that a player will be holding
 * at the poker table.
 * This class contains the information such as the list of card,
 * the number of cards and the hand ranking. This class also contains
 * the methods needed to determine the rank of a poker hand 
 */
import java.io.Serializable;
import java.util.ArrayList;

public class Hand implements Serializable {
	private ArrayList<Card> hand;
	int cardCount;
	final int MAX_CARDS = 5;
	int[] ranking;
	
	/**
	 * Constructor initialises a new empty hand
	 */
	public Hand() {
		hand = new ArrayList<Card>();
		cardCount = 0;
		ranking = new int[6];
	}
	
	//Getters and setters:

	public ArrayList<Card> getHand() {
		return hand;
	}

	public int getCardCount() {
		return cardCount;
	}
	
	/**
	 * Adds a card to the hand and updates cardCount
	 * @param card will be added to player's hand
	 */
	public void addCard(Card card){
		hand.add(card);
		cardCount++;
	}
	
	/**
	 * Resets the player's hand, removing all the cards
	 */
	public void reset(){
		hand.clear();
		cardCount = 0;
	}
	
	/**
	 * Set all the faceDown cards in the hand to faceUp
	 */
	public void setFaceUp(){
		for(Card card: hand){
			if(!card.isFaceUp())
				card.setFaceUp(true);
		}
	}
	
	/**
	 * Helper method to swap elements at i and j in the hand
	 * @param i index of element to be swapped
	 * @param j index of element to be swapped
	 */
	private void swap(int i, int j){
		Card temp;
		temp = hand.get(i); 
		hand.set(i, hand.get(j));
		hand.set(j, temp);
	}
	
	/**
	 * Create a new list of the hand sorted in decreasing order
	 * @param hand hand to be sorted
	 * @return sorted hand
	 */
	public ArrayList<Card> sortHand(ArrayList<Card> hand){
		ArrayList<Card> sortedHand = new ArrayList<Card>();
		
		//copy the contents of hand into sorted hand
		for(Card card: hand){
			Card toAdd = new Card(card.getRank(), card.getSuit());
			sortedHand.add(toAdd);
		}
		
		//use insertion sort to order the cards
		for(int i=1; i<sortedHand.size(); i++){
			for(int j=i; j>0 && sortedHand.get(j).getCardValue()>
					sortedHand.get(j-1).getCardValue(); j--){
				swap(j, j-1);
			}
		}
		
		return sortedHand;
	}
	
	/**
	 * Determine the ranking of the hand, by calling each
	 * specialised hand ranking method in order of 
	 * decreasing rank
	 * @return the corresponding ranking array of the hand
	 */
	public int[] determineHandRanking(){
		if(isRoyalFlush(hand))
			return ranking;
		else if (isStraightFlush(hand))
			return ranking;
		else if (isFourOfAKind(hand))
			return ranking;
		else if (isFullHouse(hand))
			return ranking;
		else if (isFlush(hand))
			return ranking;
		else if (isStraight(hand))
			return ranking;
		else if (isThreeOfAKind(hand))
			return ranking;
		else if (isTwoPair(hand))
			return ranking;
		else if (isPair(hand))
			return ranking;
		else{
			highCard(hand);
			return ranking;
		}
	}
	
	/**
	 * Determine the ranking of the visible cards in the hand, 
	 * by calling each specialised hand ranking method in 
	 * order of decreasing rank
	 * @return the corresponding ranking array of the hand
	 */
	public int[] determineVisibleHandRanking(){
		//create a list of only the faceUp cards in the current hand
		ArrayList<Card> visibleHand = this.visibleHand();
		
		//if the hand has less than 3 visible cards, test for Pair and High Card
		if(visibleHand.size() < 3){
			if (isPair(visibleHand))
				return ranking;
			else{
				highCard(visibleHand);
				return ranking;
			}
		} 
		//if the hand has 3 visible cards, test for 3 of a kind, Pair and High Card
		else if(visibleHand.size() == 3){
			if (isThreeOfAKind(visibleHand))
				return ranking;
			else if (isPair(visibleHand))
				return ranking;
			else{
				highCard(visibleHand);
				return ranking;
			}
		}
		
		/* if the hand has 4 visible cards, test for 4 of a kind, 
		 * 3 of a kind, 2 pair, Pair and High Card
		 */
		else if(visibleHand.size() == 4){
			if (isFourOfAKind(visibleHand))
				return ranking;
			else if (isThreeOfAKind(visibleHand))
				return ranking;
			else if (isTwoPair(visibleHand))
				return ranking;
			else if (isPair(visibleHand))
				return ranking;
			else{
				highCard(visibleHand);
				return ranking;
			}
		}

		//if none of the cases (which should not happen) simply return unmodified ranking
		return ranking;
	}
	
	/**
	 * Create an ArrayList of the faceUp cards in the hand
	 * @return ArrayList of the faceUp cards in the players hand
	 */
	public ArrayList<Card> visibleHand(){
		ArrayList<Card> visibleHand = new ArrayList<Card>();
		
		//add all faceUp cards to visibleHand
		for(Card card: hand){
			Card upCard = new Card(card.getRank(), card.getSuit());
			if(card.isFaceUp()){
				visibleHand.add(upCard);
			}
		}
		
		return visibleHand;
	}
	
	/**
	 * /**
	 * Determines if the hand is a Royal Flush
	 * @param hand arrayList of cards to be checked
	 * @return true if it is a Royal Flush, false otherwise
	 */
	public boolean isRoyalFlush(ArrayList<Card> hand){
		//first sort the cards
		ArrayList<Card> sorted = sortHand(hand);
		
		//if hand is not a flush or is not straight, return -1
		if(!isFlush(sorted) || !isStraight(sorted)){
			return false;
		}
		//if it is, check that the lowest card is a 10
		else{
			if(sorted.get(sorted.size()-1).getCardValue() == 10){
				//set the first element of the ranking array to 10
				ranking[0] = 10;
				return true;
			}
			else 
				return false;
		}
	}
	
	/**
	 * Determines if the hand is a Straight Flush
	 * @param hand arrayList of cards to be checked
	 * @return true if it is a flush, false otherwise
	 */
	public boolean isStraightFlush(ArrayList<Card> hand){
		//first sort the cards
		ArrayList<Card> sorted = sortHand(hand);
		
		//if it is not a straight or a flush, return false
		if(!isStraight(sorted) || !isFlush(sorted))
			return false;
		
		/* otherwise we have a Straight Flush 
		 * set ranking array accordingly and return true
		 */
		else{
			//set the first element of the ranking array to 9
			ranking[0] = 9;
			
			/* set the rest of the elements in the array to 
			 * the sorted values in decreasing order
			 */
			for(int i=0; i<sorted.size(); i++){
				ranking[i+1] = sorted.get(i).getCardValue();
			}
			return true;
		}
	}
	
	/**
	 * Determines if the hand is a Four of a Kind
	 * @param hand arrayList of cards to be checked
	 * @return true if it is Four of a Kind, false otherwise
	 */
	public boolean isFourOfAKind(ArrayList<Card> hand){
		//first sort the cards
		ArrayList<Card> sorted = sortHand(hand);
		
		int count = 1; //to be used to count number of repeated cards		
		int pastValue = sorted.get(0).getCardValue();
		int currentValue;
		
		for(int i = 1; i<sorted.size(); i++){
			//get the value of the current card
			currentValue = sorted.get(i).getCardValue();
			
			/* if the value of the current card is the same as the
			 * previous one, increase count 
			 */
			if(currentValue == pastValue)
				count++;
			//otherwise, reset the count
			else
				count = 1;
			
			//if the count reaches 4, then we have Four of a Kind
			if(count == 4){
				//set the first element of the ranking array to 8
				ranking[0] = 8;
				
				//set the second element in the ranking array to repeated value
				ranking[1] = currentValue;
				
				//set the third element to the value of the remaining card in the hand
				for(Card card: sorted){
					if(card.getCardValue() != currentValue)
						ranking[2] = card.getCardValue();
				}
				
				return true;
			}
			pastValue = currentValue;
		}
		//if we exit the loop, we do not have Four of a Kind
		return false;
	}
	
	/**
	 * Determines if the hand is a Full House
	 * @param hand arrayList of cards to be checked
	 * @return true if Full House, false otherwise
	 */
	public boolean isFullHouse(ArrayList<Card> hand){
		//first sort the cards
		ArrayList<Card> sorted = sortHand(hand);
		
		int count = 1;
		int totalCount = 0;
		int pastValue = sorted.get(0).getCardValue();
		int currentValue;
		
		//go through complete hand, first looking for the triple
		for(int i = 1; i<sorted.size(); i++){
			//get the value of the current card
			currentValue = sorted.get(i).getCardValue();
			
			/* if the value of the current card is the same as the
			 * previous one, increase count 
			 */
			if(currentValue == pastValue)
				count++;
			//otherwise, reset the count
			else
				count = 1;
			
			//if the count reaches 3, then we have a Triple
			if(count == 3){
				//add the current count to total count
				totalCount += count; 
				
				//reset count to check for the pair
				count = 1;
						
				//set the second value of the ranking array to the value of the triple
				ranking[1] = currentValue;
			}
			pastValue = currentValue;
		}
		
		//now go through complete hand again, looking for the double
		for(int i = 1; i<sorted.size(); i++){
			//get the value of the current card
			currentValue = sorted.get(i).getCardValue();
			
			/* if the value of the current card is the same as the
			 * previous one, and that card is not one of the triples
			 * increase the count value
			 */
			if(currentValue == pastValue && currentValue != ranking[1])
				count++;
			//otherwise, reset the count
			else
				count = 1;
			
			//if the count reaches 2, then we have a Pair
			if(count == 2){
				//add the current count to total count
				totalCount += count; 
				
				//reset count to check for the triple
				count = 1;
						
				//set the third value of the ranking array to the value of the pair
				ranking[2] = currentValue;
			}
			pastValue = currentValue;
		}
		
		//if we exit the loop and totalCount is 5, we have a full house
		if(totalCount == 5){
			//set the first element of the ranking array to 7
			ranking[0] = 7;
			
			return true;
		}

		//otherwise we do not have a Full House
		else{
			return false;
		}
	}
	
	/**
	 * Determines if the hand is a Flush
	 * @param hand arrayList of cards to be checked
	 * @return true if it is a flush, false otherwise
	 */
	public boolean isFlush(ArrayList<Card> hand){
		//first sort the cards
		ArrayList<Card> sorted = sortHand(hand);
		
		//get the suit of the first card
		Card.CardSuitEnum suit = sorted.get(0).getSuit();
		
		/* compare the suit of each card with suit
		 * the moment a card has a different suit, return -1
		 */
		for(int i=1; i<sorted.size(); i++){
			if(sorted.get(i).getSuit() != suit)
				return false;
		}
		
		/* if the loop is exited, we have a Flush
		 * set ranking array accordingly and return true
		 * set first element of array to 6
		 */
		ranking[0] = 6;
		
		/* set the rest of the elements in the array to 
		 * the hand values in decreasing order
		 */
		for(int i=0; i<sorted.size(); i++){
			ranking[i+1] = sorted.get(i).getCardValue();
		}
		return true;
		
	}
	
	/**
	 * Determines if the hand is a Straight
	 * @param hand arrayList of cards to be checked
	 * @return true if it is a straight, false otherwise
	 */
	public boolean isStraight(ArrayList<Card> hand){
		//first sort the cards
		ArrayList<Card> sorted = sortHand(hand);
		
		/* if the hand contains an ACE we must check if it is
		 * part of a straight as a HIGH (14) or LOW (1) value
		 */
		if(this.containsAce()){
			//ace will count as 1 if the next highest card is a FIVE
			if(sorted.get(1).getCardValue() == 5){
				
				/* check that each card (excluding the first, 
				 * which is an ace) follows the other
				 */
				for(int i=2; i<sorted.size(); i++){
					/* if the previous card is not exactly one more than
					 * than the current card, we do not have a straight
					 */
					if(sorted.get(i-1).getCardValue() != 
							sorted.get(i).getCardValue() + 1){
						return false;
					}
				}
				/* if loop is exited we have a straight
				 * adjust array: first value is 5, the remaining 
				 * values are 5, 4, 3, 2, 1
				 */
				ranking = new int[]{5, 5, 4, 3, 2, 1};
				return true;
			}
			
			//ace will count as 14 if next highest card is a KING
			else if(sorted.get(1).getCardValue() == 13){

				// check that each card follows the other
				for(int i=1; i<sorted.size(); i++){
					/* if the previous card is not exactly one more than
					 * than the current card, we do not have a straight
					 */
					if(sorted.get(i-1).getCardValue() != 
							sorted.get(i).getCardValue() + 1){
						return false;
					}
				}
				/* if loop is exited we have a straight
				 * adjust array: first value is 5, the remaining 
				 * values are 14, 13, 12, 11, 10
				 */
				ranking = new int[]{5, 14, 13, 12, 11, 10};
				return true;
			}
			
			//if we have an ACE and no FIVE or KING, there is no straight 
			else
				return false;
		}
		
		//if there is no ace, proceed normally
		else{
			//check that each card follows the other
			for(int i=1; i<sorted.size(); i++){
				/* if the previous card is not exactly one more than
				 * than the current card, we do not have a straight
				 */
				if(sorted.get(i-1).getCardValue() != 
						sorted.get(i).getCardValue() + 1){
					return false;
				}
			}
			/* if we exit the loop, we have a straight
			 * set the ranking array accordingly and return true
			 * set the first element to 5
			 */
			ranking[0] = 5;
			
			/* set the rest of the elements in the array to 
			 * the hand values in decreasing order
			 */
			for(int i=0; i<sorted.size(); i++){
				ranking[i+1] = sorted.get(i).getCardValue();
			}
			
			return true;
		}

	}
	
	/**
	 * Helper method to check if the hand contains an Ace
	 * @return true if hand contains an ace, false otherwise
	 */
	private boolean containsAce(){
		//if an ace (card of value 14) is found, return true
		for(Card card: hand){
			if(card.getCardValue() == 14)
				return true;
		}
		
		//if we exit loop, there is no ace
		return false;
	}
	
	/**
	 * Determines if the hand is a Three of a Kind
	 * @param hand arrayList of cards to be checked
	 * @return true if it is a Three of a Kind, false otherwise
	 */
	public boolean isThreeOfAKind(ArrayList<Card> hand){
		//first sort the cards
		ArrayList<Card> sorted = sortHand(hand);
		
		int count = 1; //to be used to count number of repeated cards		
		int pastValue = sorted.get(0).getCardValue();
		int currentValue;
		
		for(int i = 1; i<sorted.size(); i++){
			//get the value of the current card
			currentValue = sorted.get(i).getCardValue();
			
			/* if the value of the current card is the same as the
			 * previous one, increase count 
			 */
			if(currentValue == pastValue)
				count++;
			//otherwise, reset the count
			else
				count = 1;
			
			//if the count reaches 3, then we have Three of a Kind
			if(count == 3){
				//set the first element of the ranking array to 4
				ranking[0] = 4;
				
				//set the second element in the ranking array to repeated value
				ranking[1] = currentValue;
				
				/* set the third and fourth element to the value of the 
				 * remaining cards in the hand in decreasing order
				 */
				int k = 2; //will be used in for loop to assign index 2 and 3
				for(Card card: sorted){ 
					if(card.getCardValue() != currentValue){
						ranking[k] = card.getCardValue();
						k++;
					}
				}
				return true;
			}
			pastValue = currentValue;
		}
		//if we exit the loop, we do not have Three of a Kind
		return false;
	}
	
	
	/**
	 * Determines if the hand is a Two Pair
	 * @param hand arrayList of cards to be checked
	 * @return true if it is a Two Pair, false otherwise
	 */
	public boolean isTwoPair(ArrayList<Card> hand){
		//first sort the cards
		ArrayList<Card> sorted = sortHand(hand);
		
		int count = 1;
		int totalCount = 0;
		int pastValue = sorted.get(0).getCardValue();
		int currentValue;
		
		for(int i = 1; i<sorted.size(); i++){
			//get the value of the current card
			currentValue = sorted.get(i).getCardValue();
			
			/* if the value of the current card is the same as the
			 * previous one, increase count 
			 */
			if(currentValue == pastValue)
				count++;
			//otherwise, reset the count
			else
				count = 1;
			
			//if the count reaches 2, then we have a Pair
			if(count == 2){
				//add the current count to total count
				totalCount += count; 
				
				//reset count to check for another pair
				count = 1;
				
				//if this is the first pair we find:
				if(totalCount == 2){
					//set the second element in ranking array to currentValue
					ranking[1] = currentValue;
				}
				
				//if this is the second pair we find:
				if(totalCount == 4){
					//set the first element of the ranking array to 3
					ranking[0] = 3;
					
					//set the third element in ranking array to currentValue
					ranking[2] = currentValue;
					
					//set fourth element to value of remaining card
					for(Card card: sorted){ 
						if(card.getCardValue() != currentValue && 
								card.getCardValue() != ranking[1]){
							ranking[3] = card.getCardValue();
						}
					}
					return true;
				}
			}
			pastValue = currentValue;
		}
		//if we exit the loop, we do not have a Two Pair
		return false;
	}
	
	/**
	 * Determines if the hand is a Pair
	 * @param hand arrayList of cards to be checked
	 * @return true if it is a Pair, false otherwise
	 */
	public boolean isPair(ArrayList<Card> hand){
		//first sort the cards
		ArrayList<Card> sorted = sortHand(hand);
		
		int count = 1; //to be used to count number of repeated cards		
		int pastValue = sorted.get(0).getCardValue();
		int currentValue;
		
		for(int i = 1; i<sorted.size(); i++){
			//get the value of the current card
			currentValue = sorted.get(i).getCardValue();
			
			/* if the value of the current card is the same as the
			 * previous one, increase count 
			 */
			if(currentValue == pastValue)
				count++;
			//otherwise, reset the count
			else
				count = 1;
			
			//if the count reaches 1, then we have a Pair
			if(count == 2){
				//set the first element of the ranking array to 2
				ranking[0] = 2;
				
				//set the second element in the ranking array to repeated value
				ranking[1] = currentValue;
				
				/* set the third, fourth and fifth elements to the values
				 * of the remaining cards in the hand in decreasing order
				 */
				int k = 2; //will be used in for loop to assign index 2, 3 and 4
				for(Card card: sorted){ 
					if(card.getCardValue() != currentValue){
						ranking[k] = card.getCardValue();
						k++;
					}
				}
				return true;
			}
			pastValue = currentValue;
		}
		//if we exit the loop, we do not have a Pair
		return false;
	}
	
	/**
	 * Sets the first value of ranking array to 1
	 * and puts the card values in decreasing order
	 * into the other indices of the array
	 * @param hand arrayList of cards to be checked
	 */
	public void highCard(ArrayList<Card> hand){
		//first sort the cards
		ArrayList<Card> sorted = sortHand(hand);
		
		//set the first element of the ranking array to 1
		ranking[0] = 1;
		
		/* set the rest of the elements in the array to 
		 * the hand values in decreasing order
		 */
		for(int i=0; i<sorted.size(); i++){
			ranking[i+1] = sorted.get(i).getCardValue();
		}
	}
	
	@Override
	public String toString(){
		String handString = "";
		for(Card card: hand){
			if(!card.isFaceUp()){
				handString += "(D)";
			}
			handString += ""+card.getCardValue()+" of "+card.getSuit()+" \t";
		}
		
		return handString;
	}
	
	/**
	 * @return the string saying what the ranking of the hand is
	 */
	public String rankingString(){
		String rankString = "";
		int rank = ranking[0];
		
		switch(rank){
			case 1: rankString += "High Card "+ranking[1];
				break;
			
			case 2: rankString += "Pair of "+ranking[1]+"s";
				break;
				
			case 3: rankString += "Two Pair "+ranking[1]+" & "+ranking[2];
				break;
				
			case 4: rankString += "Three of a Kind "+ranking[1];
				break;
				
			case 5: rankString += "Straight (highest card is "+ranking[1]+")";
				break;
				
			case 6: rankString += "Flush (highest card is "+ranking[1]+")";
				break;
				
			case 7: rankString += "Full House (triple of "+ranking[1]+", pair of "+ranking[2]+")";
				break;
				
			case 8: rankString += "Four of a Kind "+ranking[1];
				break;
			
			case 9: rankString += "Straight Flush (highest card is "+ranking[1]+")";
				break;
				
			case 10: rankString += "ROYALLY FLUSHED!!!";
				break;
		}
		
		return rankString;
	}
}
